package service;

import java.util.LinkedList;

import org.apache.commons.lang3.StringUtils;

import enums.Symbols;
import exception.XMLParseException;
import model.XML;
import util.Utils;

/**
 * @author Vitaly Vrublevskyy
 * 
 */
public class Parser {

	private String plainText;
	private int caretPosition;
	private LinkedList<XML> stackWithNodes;

	/**
	 * Instantiates parser by providing XML plain text. 
	 * During instantiation provided plain text in being filtered for redundant chars.
	 * 
	 * @param string plain text that represents XML.
	 */
	public Parser(final String string) {
		this.plainText = Utils.removeRedundantChars(string);
		this.stackWithNodes = new LinkedList<XML>();
		this.caretPosition = 0;
	}

	/**
	 * Processes plain text that represents XML and returns appropriate node(s)structure. 
	 * Internally processes each node step-by-step by extracting value, attributes and text value of the node.
	 * 
	 * @return Root element
	 * @throws XMLParseException
	 */
	public XML process() throws XMLParseException {
		checkIsEmptyText();

		XML curNode = null;
		String chunk;
		String text;
		int nextPosition;
		int textLength = plainText.length();

		do {
			if (isLTChar(caretPosition)) { 								// "<"
				nextPosition = getNearestRTCharIndex(caretPosition); 	// ">"
				text = getNodeTextValueByIndex(nextPosition + 1); 		// text value between ">" ... "<"
				if (nextIsSlashChar(caretPosition)) {
					validateByNames();
					curNode = stackWithNodes.removeLast();
					populateParent(curNode, text);
				} else {
					chunk = getNodeByIndex(caretPosition); 				// content between "<" ... ">" (name and attributes)
					curNode = new XML(chunk);
					if (previousIsSlashChar(nextPosition)) {
						// Check is single root XML
						if (stackWithNodes.isEmpty()) {
							return curNode;
						}
						// Is single tag (ignore pushing to stack)
						populateParent(curNode, text);
					} else {
						// Is common open tag
						stackWithNodes.addLast(curNode);
						curNode.insertTextValue(text);
					}
				}
				
				caretPosition = nextPosition;
			}
			
			caretPosition++;

		} while (caretPosition < textLength);

		return curNode; // as XML root node
	}

	/**
	 * Verifies that provided XML file is empty.
	 * 
	 * @throws XMLParseException when provided text is empty
	 */
	private void checkIsEmptyText() throws XMLParseException {
		if (StringUtils.isBlank(plainText)) {
			throw new XMLParseException("Invalid XML: XML file is empty");
		}
	}

	/**
	 * Compare pair names of open and close tags If its different - throw custom exception and stop parsing of XML
	 * 
	 * @throws XMLParseException
	 * */
	private void validateByNames() throws XMLParseException {
		String curName = getNodeByIndex(caretPosition + 1).trim();
		String prevName = stackWithNodes.getLast().getName();
		if (!curName.equals(prevName)) {
			throw new XMLParseException("Invalid XML structure: Different names of open and close tags");
		}
	}

	/**
	 * Add child to parent hierarchical collection
	 * 
	 * @param curNode
	 */
	private void populateParent(XML curNode, String text) {
		if (!stackWithNodes.isEmpty()) {
			XML parent = stackWithNodes.getLast();
			curNode.setParent(parent); // reference to the parent node
			parent.addChild(curNode); // add to parent hierarchical collection
			parent.insertTextValue(text); //hold text value
		}
	}

	private boolean nextIsSlashChar(int i) {
		String slash = Symbols.SLASH.getValue();
		String symbol = Character.toString(plainText.charAt(i + 1));

		return (i < plainText.length() - 1) && (slash.equals(symbol));
	}

	private boolean previousIsSlashChar(int i) {
		String slash = Symbols.SLASH.getValue();
		String symbol = Character.toString(plainText.charAt(--i));
		
		return slash.equals(symbol);
	}

	private int getNearestRTCharIndex(int fromIndex) {
		return plainText.indexOf(Symbols.GT.getValue(), fromIndex);
	}

	private String getNodeByIndex(int beginIndex) {
		int endIndex = getNearestRTCharIndex(beginIndex);
		if (previousIsSlashChar(endIndex)) {
			endIndex--;
		}
		
		return plainText.substring(++beginIndex, endIndex).trim();
	}

	private boolean isLTChar(int position) {
		String lt = Symbols.LT.getValue();
		String symbol = Character.toString(plainText.charAt(position));
		
		return lt.equals(symbol);
	}

	/**
	 * Get text value between ">" ... "<"
	 * 
	 * @param startIndex caret position
	 * @return
	 */
	private String getNodeTextValueByIndex(int startIndex) {
		String result = "";
		if (startIndex < plainText.length() - 1) {
			int endIndex = plainText.indexOf(Symbols.LT.getValue(), startIndex);
			result = plainText.substring(startIndex, endIndex);
		}
		
		return result.trim();
	}
}
